iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 29 篇

Day 29|Middleware 與 Global Exception Handling

  • 分享至 

  • xImage
  •  

Day 28 我們把 Product API 的責任拆開:

ProductsController
→ 處理 HTTP Request / Response

ProductService
→ 處理 Product 操作

並透過 Dependency Injection,讓 Controller 可以取得需要的 Service。

接下來會遇到另一個問題:

如果每個 Controller 都需要處理相同的 Exception,難道每一支 Action 都要重複寫 try-catch 嗎?

例如:

try
{
    // 執行程式
}
catch (Exception)
{
    // 回傳錯誤
}

如果每個 Controller 都這樣寫:

ProductsController
→ try-catch

OrdersController
→ try-catch

UsersController
→ try-catch

很快就會出現大量重複程式碼。

ASP.NET Core 提供了一個更適合處理這種共用流程的機制:

Middleware

今天會從 Middleware 開始,最後實作:

Global Exception Handling

讓未處理的 Exception 可以在 Application 中統一處理。


1. Middleware 與 Request Pipeline

當 Client 發送:

GET /api/products/1

Request 並不是直接進入 Controller。

它會經過一連串處理流程,這整條流程稱為:

Request Pipeline
→ 請求處理管線

而 Pipeline 裡每一個負責處理 Request / Response 的元件,就是:

Middleware

可以先把 Middleware 想成:

HTTP Request 前往 Controller 時會經過的一層層處理站。

常見的 Middleware 可能負責:

Exception Handling
HTTPS Redirection
Authentication
Authorization

Middleware 有一個很重要的特性:

Request
↓
Middleware A
↓
Middleware B
↓
Controller
↑
Middleware B
↑
Middleware A
↑
Response

也就是:

Middleware 可以在下一層執行前處理 Request,也可以在下一層執行完成後處理 Response。

因此 Middleware 可以包住後面的 Pipeline。

這也是 Global Exception Handling 能運作的重要原因。


2. app.Use...:把 Middleware 加進 Pipeline

其實我們之前的 Program.cs 已經看過 Middleware:

app.UseHttpsRedirection();

可以先理解成:

app.Use...
→ 把 Middleware 加進 Request Pipeline

例如:

var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

今天會再加入:

app.UseExceptionHandler();

把 Exception Handling 加進 Pipeline。


3. 為什麼不要每個 Controller 都寫 try-catch?

假設:

[HttpGet("{id:int}")]
public ActionResult<Product> GetById(int id)
{
    try
    {
        Product? product =
            _productService.GetById(id);

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }
    catch (Exception)
    {
        return StatusCode(500);
    }
}

如果每個 Action 都這樣做:

GetAll()
→ try-catch

GetById()
→ try-catch

Create()
→ try-catch

Update()
→ try-catch

Delete()
→ try-catch

Controller 又會開始累積大量重複邏輯。

這些 Action 做的其實都是同一件事:

發生未處理 Exception
↓
回傳 HTTP 500

所以更好的責任分工是:

Controller
→ 處理正常流程

Global Exception Handler
→ 統一處理未處理 Exception

4. 建立 GlobalExceptionHandler

今天會使用 ASP.NET Core 提供的:

IExceptionHandler

建立統一的 Exception Handler。

專案結構:

MyApi2
│
├─ Controllers
│  └─ ProductsController.cs
│
├─ Models
│  ├─ Product.cs
│  ├─ CreateProductRequest.cs
│  └─ UpdateProductRequest.cs
│
├─ Services
│  ├─ IProductService.cs
│  └─ ProductService.cs
│
├─ Exceptions
│  └─ GlobalExceptionHandler.cs
│
└─ Program.cs

建立:

Exceptions/GlobalExceptionHandler.cs

完整程式碼:

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace MyApi2.Exceptions;

public class GlobalExceptionHandler
    : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken
    )
    {
        ProblemDetails problemDetails = new()
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Internal Server Error",
            Detail = "伺服器發生未預期的錯誤。"
        };

        httpContext.Response.StatusCode =
            StatusCodes.Status500InternalServerError;

        await httpContext.Response.WriteAsJsonAsync(
            problemDetails,
            cancellationToken
        );

        return true;
    }
}

今天先看懂四個重點:

IExceptionHandler
→ 接收未處理 Exception

ProblemDetails
→ 建立統一錯誤格式

TryHandleAsync()
→ 執行 Exception 處理

return true
→ 表示這個 Exception 已經被處理

也就是說:

TryHandleAsync() 回傳 true,代表目前這個 Handler 已經完成 Exception 的處理。


5. ProblemDetails 是什麼?

剛才建立:

ProblemDetails problemDetails = new()
{
    Status = StatusCodes.Status500InternalServerError,
    Title = "Internal Server Error",
    Detail = "伺服器發生未預期的錯誤。"
};

它可以讓 API Error Response 有一致的格式。

例如:

{
  "title": "Internal Server Error",
  "status": 500,
  "detail": "伺服器發生未預期的錯誤。"
}

這比只回傳:

500

更容易讓 Client 理解發生了什麼事情。


6. Program.cs:註冊 Exception Handler

接著修改:

Program.cs

完整程式碼:

using MyApi2.Exceptions;
using MyApi2.Services;

var builder =
    WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<
    IProductService,
    ProductService
>();

builder.Services.AddProblemDetails();

builder.Services.AddExceptionHandler<
    GlobalExceptionHandler
>();

var app = builder.Build();

app.UseExceptionHandler();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

今天新增三個重要位置。

第一個:

builder.Services.AddProblemDetails();

註冊 Problem Details 相關服務。

第二個:

builder.Services.AddExceptionHandler<
    GlobalExceptionHandler
>();

註冊我們建立的 GlobalExceptionHandler。

第三個:

app.UseExceptionHandler();

把 Exception Handler Middleware 加進:

Request Pipeline

整體可以理解成:

註冊 Handler
↓
加入 Middleware Pipeline
↓
發生未處理 Exception
↓
GlobalExceptionHandler 處理

7. 實際測試 Global Exception Handling

為了觀察效果,可以暫時在:

ProductsController.cs

加入:

[HttpGet("error")]
public IActionResult TestError()
{
    throw new InvalidOperationException(
        "測試 Exception"
    );
}

接著執行:

GET /api/products/error

Controller 會丟出:

InvalidOperationException

因為這個 Exception 沒有在 Controller 裡處理,所以會繼續往外傳。

最後 Client 會收到:

HTTP 500 Internal Server Error

以及類似:

{
  "title": "Internal Server Error",
  "status": 500,
  "detail": "伺服器發生未預期的錯誤。"
}

Controller 不需要為這類未處理 Exception,在每一個 Action 重複撰寫 try-catch。

這就是 Global Exception Handling 的價值。


8. NotFound() 也是 Exception 嗎?

不是。

之前我們寫:

Product? product =
    _productService.GetById(id);

if (product is null)
{
    return NotFound();
}

這裡的:

return NotFound();

只是 Controller 主動決定:

Product 不存在
↓
回傳 HTTP 404

它不是 Exception。

所以:

404 Not Found
≠
Exception

例如:

情況 處理方式
Product 找不到 NotFound()
Request 不合法 Validation / BadRequest()
建立成功 CreatedAtAction()
未處理 Exception Global Exception Handler

可以先這樣理解:

可預期的 HTTP 結果
→ Controller / ASP.NET Core
→ 回傳對應 HTTP Status

未處理 Exception
→ Global Exception Handler

例如 Day 27 學過的 Validation,在使用 [ApiController] 時,ASP.NET Core 可以直接回傳:

400 Bad Request

不一定需要 Controller 自己手動:

return BadRequest();

所以 Global Exception Handling 並不是把所有 HTTP Error 都變成 Exception。

它主要負責:

處理沒有被其他地方處理的 Exception。


9. Middleware 的順序為什麼重要?

目前:

app.UseExceptionHandler();

app.UseHttpsRedirection();

app.MapControllers();

可以理解成:

Request
↓
Exception Handler Middleware
↓
其他 Middleware
↓
Controller

Exception Handler 放在比較前面,就能包住後面的處理流程。

如果後面的 Controller 或 Service 發生未處理 Exception:

Request
↓
Exception Handler
↓
Controller / Service
↓
Exception ✖
↑
Exception Handler 處理
↓
Error Response

因此:

Middleware 是按照加入 Pipeline 的順序執行的。

順序不同,能處理到的範圍也可能不同。


10. 一次看懂今天的完整流程

今天只需要掌握四個步驟:

註冊 Exception Handler
↓
加入 Middleware Pipeline
↓
捕捉未處理 Exception
↓
統一產生 Error Response

正常情況:

HTTP Request
↓
Middleware
↓
Controller
↓
Service
↓
HTTP Response

如果發生未處理 Exception:

HTTP Request
↓
Exception Handler Middleware
↓
Controller / Service
↓
Exception
↓
GlobalExceptionHandler
↓
500 ProblemDetails
↓
HTTP Response

對應到程式碼,可以看三個位置。

第一個:

builder.Services.AddExceptionHandler<
    GlobalExceptionHandler
>();

代表:

註冊

第二個:

app.UseExceptionHandler();

代表:

加入 Pipeline

第三個:

public async ValueTask<bool> TryHandleAsync(...)

代表:

統一處理 Exception

Day 29 小結

今天最重要的是理解:

Middleware 用來處理 Application 共用的 Request / Response 流程。

而:

Global Exception Handling 將未處理的 Exception 集中處理,避免相同的 try-catch 散落在各個 Controller。

最後記住一句話:

Middleware 解決的是「共用流程放在哪裡」;Global Exception Handling 解決的是「未處理 Exception 要在哪裡統一處理」。


上一篇
Day 28|Dependency Injection
系列文
現在就學C# 與 ASP.NET Core 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言